package server;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Date;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import server.messageContent.BalanceEnquiryMsgContent;
import server.messageContent.BalanceEnquiryReplyMsgContent;
import server.messageContent.CloseAccountMsgContent;
import server.messageContent.CloseReplyMsgContent;
import server.messageContent.CreateAccountMsgContent;
import server.messageContent.CreateAccountReplyMsgContent;
import server.messageContent.DepositMsgContent;
import server.messageContent.DepositReplyMsgContent;
import server.messageContent.MonitorReplyMsgContent;
import server.messageContent.MonitorMsgContent;
import server.messageContent.TransferFundMsgContent;
import server.messageContent.TransferFundReplyMsgContent;
import server.messageContent.WithdrawMsgContent;
import server.messageContent.WithdrawReplyMsgContent;
import common.CurrencyEnum;
import common.InvocationTypeEnum;
import common.ServiceErrorException;
import common.ServiceResponseEnum;
import common.data.Message;
import common.data.MessageTypeEnum;
import common.data.MonitorReplyTypeEnum;
import common.data.RequestTypeEnum;

public class RequestHandler {
	private AccountServiceImpl accountService;
	private static long replyId = 0;
	private static Callback monitor;
	private Timer timer;
	boolean duplicateRequest;

	private class UID { // Unique Identifier
		String clientAddress;
		long requestID;
		RequestTypeEnum requestType;

		public UID(String newAddress, long newRequestID, RequestTypeEnum newrequestType) {
			this.clientAddress = newAddress;
			this.requestID = newRequestID;
			this.requestType = newrequestType;
		}

	}

	List<UID> UIDList;

	private class UniqueReply {
		UID uid;
		Message content;

		public UniqueReply(UID newUID, Message newContent) {
			uid = newUID;
			content = newContent;
		}

	}

	List<UniqueReply> replyList;

	int UIDListLength = 0;
	int UniqueReplyLength = 0;
	private CloseReplyMsgContent closeAccountReplyMsg;

	public RequestHandler() {
		this.accountService = AccountServiceImpl.getCurrent();
		this.UIDList = new ArrayList<UID>();
		this.replyList = new ArrayList<UniqueReply>();
		this.timer = new Timer();
	}

	public DatagramPacket HandleRequest(DatagramPacket request,
			InvocationTypeEnum invo) throws SocketException, IOException,
			ServiceErrorException {
		byte[] data = request.getData();
		Message msg = new Message(data);
		Message reply = null;
		int i = 0;
		String clientAddress;
		long requestID;
		long accId = 0;
		RequestTypeEnum requestType = null;
		int response;
		String password;
		String holderName;
		DatagramPacket result = null;
		CurrencyEnum currency = null;
		switch (invo) {
		case AT_LEAST_ONCE:
			duplicateRequest = false; //don't need to check duplicated requests
			break;
		case AT_MOST_ONCE:
			duplicateRequest = checkDuplicateUID(request); //need to check 1st before execute request
			break;
		}
		System.out.println(invo.getMsg());
		System.out.println("Duplication is " + String.valueOf(duplicateRequest));

		if (/*msg.getRequestType() != RequestTypeEnum.BALANCE_ENQUIRY
				&&*/ !duplicateRequest) {  
			switch (msg.getRequestType()) {
			case CREATE_ACCOUNT:
				// convert msgContent to information obj
				CreateAccountMsgContent createAccountMsg = (CreateAccountMsgContent) msg
						.getMsgContent();
				// handle request
				accId = this.createAccount(createAccountMsg);
				System.out.println("Created account with ID " + accId);
				// create the reply msg
				CreateAccountReplyMsgContent createAccountReplyMsg = new CreateAccountReplyMsgContent(
						accId);

				reply = new Message(MessageTypeEnum.REPLY,
						RequestHandler.replyId++, createAccountReplyMsg);
				result = new DatagramPacket(reply.serialize(),
						reply.getLength(), request.getAddress(),
						request.getPort());
				break;
			case CLOSE_ACCOUNT:
				CloseAccountMsgContent closeAccountMsg = (CloseAccountMsgContent) msg
						.getMsgContent();
				accId = closeAccountMsg.getAccNumber();
				password = closeAccountMsg.getPassword();
				holderName = closeAccountMsg.getHolderName();
				System.out.println("Account Number:" + accId + password
						+ holderName);
				response = this.closeAccount(holderName, closeAccountMsg,
						password);
				if (response == 1) {
					closeAccountReplyMsg = new CloseReplyMsgContent(
							ServiceResponseEnum.CLOSE_SUCCESSFUL, accId);
				} else if (response == -1) {
					closeAccountReplyMsg = new CloseReplyMsgContent(
							ServiceResponseEnum.NON_EXISTING_ACCOUNT, accId);
				} else if (response == -2) {
					closeAccountReplyMsg = new CloseReplyMsgContent(
							ServiceResponseEnum.WRONG_PASSWORD, accId);
				}
				reply = new Message(MessageTypeEnum.REPLY,
						RequestHandler.replyId++, closeAccountReplyMsg);
				result = new DatagramPacket(reply.serialize(),
						reply.getLength(), request.getAddress(),
						request.getPort());
				break;
			case DEPOSIT_ACCOUNT:
				DepositMsgContent depositAccountMsg = (DepositMsgContent) msg
						.getMsgContent();
				reply = new Message();
				password = depositAccountMsg.getPassword();
				float amountDeposit = depositAccountMsg.getDepositAmount();
				accId = depositAccountMsg.getAccountNumber();
				currency = depositAccountMsg.getCurrency();
				response = this.depositAccount(depositAccountMsg,
						amountDeposit, password);
				DepositReplyMsgContent depositReplyMsg = null;
				if (response == 1) {
					depositReplyMsg = new DepositReplyMsgContent(
							ServiceResponseEnum.SUCCESSFUL_DEPOSIT, accId,
							amountDeposit, currency);
				} else if (response == -1) {
					depositReplyMsg = new DepositReplyMsgContent(
							ServiceResponseEnum.NON_EXISTING_ACCOUNT, accId,
							amountDeposit, currency);

				}
				if (response == -2) {
					depositReplyMsg = new DepositReplyMsgContent(
							ServiceResponseEnum.WRONG_PASSWORD, accId,
							amountDeposit, currency);

				}
				reply = new Message(MessageTypeEnum.REPLY,
						RequestHandler.replyId++, depositReplyMsg);
				result = new DatagramPacket(reply.serialize(),
						reply.getLength(), request.getAddress(),
						request.getPort());

				break;

			case TRANSFER_SERVICE:
				TransferFundMsgContent transferMsgContent = (TransferFundMsgContent) msg
						.getMsgContent();
				ServiceResponseEnum res = this
						.transferService(transferMsgContent);
				accId = transferMsgContent.getSenderAccId();
				long raccId = transferMsgContent.getRecipientAccId();
				float amount = transferMsgContent.getAmount();
				currency = transferMsgContent.getCurrency();
				TransferFundReplyMsgContent transferReplyMsg = new TransferFundReplyMsgContent(
						res, accId,raccId,amount, currency);				
						reply = new Message(MessageTypeEnum.REPLY,
						RequestHandler.replyId++, transferReplyMsg);
				result = new DatagramPacket(reply.serialize(),
						reply.getLength(), request.getAddress(),
						request.getPort());
				break;
			case WITHDRAW_ACCOUNT:
				WithdrawMsgContent withdrawAccountMsg = (WithdrawMsgContent) msg
						.getMsgContent();
				accId = withdrawAccountMsg.getAccNumber();
				float amountWithdraw = withdrawAccountMsg.getWithdrawAmount();
				password = withdrawAccountMsg.getPassword();
				response = this.withdrawAccount(withdrawAccountMsg,
						amountWithdraw, password);
				WithdrawReplyMsgContent withdrawReplyMsg = null;
				currency = withdrawAccountMsg.getCurrency();
				if (response == -1) {
					withdrawReplyMsg = new WithdrawReplyMsgContent(
							ServiceResponseEnum.NON_EXISTING_ACCOUNT, accId,
							amountWithdraw, currency);
				} else if (response == 1) {
					withdrawReplyMsg = new WithdrawReplyMsgContent(
							ServiceResponseEnum.SUCCESSFUL_WITHDRAW, accId,
							amountWithdraw, currency);
				} else if (response == 0) {
					withdrawReplyMsg = new WithdrawReplyMsgContent(
							ServiceResponseEnum.LOW_BALANCE, accId,
							amountWithdraw, currency);
				} else if (response == -2) {
					withdrawReplyMsg = new WithdrawReplyMsgContent(
							ServiceResponseEnum.WRONG_PASSWORD, accId,
							amountWithdraw, currency);
				}

				reply = new Message(MessageTypeEnum.REPLY,
						RequestHandler.replyId++, withdrawReplyMsg);
				result = new DatagramPacket(reply.serialize(),
						reply.getLength(), request.getAddress(),
						request.getPort());
				break;
			case MONITOR_SERVER:
				// get the ipAddr, port and monitor interval from msg
				MonitorMsgContent monitorTime = (MonitorMsgContent) msg
						.getMsgContent();

				long time = monitorTime.getTime();

				InetAddress ip = request.getAddress();
				int port = request.getPort();
				// send acknowledge to client, saying that it is now monitoring
				// server
				MonitorReplyMsgContent moniterReplyMsg = new MonitorReplyMsgContent(
						MonitorReplyTypeEnum.OPEN);
				reply = new Message(MessageTypeEnum.REPLY,
						RequestHandler.replyId++, moniterReplyMsg);
				result = new DatagramPacket(reply.serialize(),
						reply.getLength(), ip, port);
				SocketManager.getCurrent().SendPacket(result);
				// Call MonitorServer method to start the timer
				MonitorServer(ip, port, time);
				result = null;
				break;
			default:
				break;
			case BALANCE_ENQUIRY:
				BalanceEnquiryMsgContent balEnquiryMsg = (BalanceEnquiryMsgContent) msg
						.getMsgContent();
				password = balEnquiryMsg.getPassword();
				accId = balEnquiryMsg.getAccountNumber();
				float res1 = this.enquiryAccount(balEnquiryMsg, password);
				BalanceEnquiryReplyMsgContent balEnquiryReplyMsg = null;
				if (res1 == -1) {
					balEnquiryReplyMsg = new BalanceEnquiryReplyMsgContent(
							ServiceResponseEnum.NON_EXISTING_ACCOUNT, accId);
				} else if (res1 == -2) {
					balEnquiryReplyMsg = new BalanceEnquiryReplyMsgContent(
							ServiceResponseEnum.WRONG_PASSWORD, accId);

				} else {
					balEnquiryReplyMsg = new BalanceEnquiryReplyMsgContent(
							ServiceResponseEnum.SUCCESS_BALANCE_ENQUIRY, accId);
				}
				reply = new Message(MessageTypeEnum.REPLY,
						RequestHandler.replyId++, balEnquiryReplyMsg);
				result = new DatagramPacket(reply.serialize(),
						reply.getLength(), request.getAddress(),
						request.getPort());

				break;
			}
			//System.out.println("Requsest ID is " + msg.getRequestId());
			UID requestUID = new UID(request.getAddress().toString()
					.replaceFirst("/", ""), msg.getRequestId(), msg.getRequestType());
			//System.out.println("In reply list, request type is " + msg.getRequestType());
			UniqueReply newReply = new UniqueReply(requestUID, reply);
			replyList.add(newReply);
			UniqueReplyLength++;
			/*System.out.println("UniqueReplyLength is " + UniqueReplyLength);
			System.out.println("ReplyListLength is " + replyList.size());*/
		} else /*if (msg.getRequestType() != RequestTypeEnum.BALANCE_ENQUIRY
				&& duplicateRequest)*/ {
			System.out.println("duplicate request");
			clientAddress = request.getAddress().toString()
					.replaceFirst("/", "");

			requestID = msg.getRequestId();
			requestType = msg.getRequestType();
			/*System.out.println("UniqueReplyLength is " + UniqueReplyLength);
			System.out.println("ReplyListLength is " + replyList.size());*/
			for (i = 0; i < UniqueReplyLength; i++) {
				//System.out.println("For i = " + i);
/*				System.out.println("Request Type is " + requestType);
				System.out.println("Request Type in the list is is " + replyList.get(i).uid.requestType);*/
				/*System.out.println("Address is " +  replyList.get(i).uid.clientAddress.equals(clientAddress));
				System.out.println("RequestID is " +  (replyList.get(i).uid.requestID == requestID));
				System.out.println("RequestType is " +  replyList.get(i).uid.requestType.equals(requestType));*/
				
				if (replyList.get(i).uid.clientAddress.equals(clientAddress)
						&& replyList.get(i).uid.requestID == requestID
						&& replyList.get(i).uid.requestType.equals(requestType)) {
					/*System.out.println("Request Type is " + requestType);
					System.out.println("Request Type is " + replyList.get(i).uid.requestType);*/
					result = new DatagramPacket(
							replyList.get(i).content.serialize(),
							replyList.get(i).content.getLength(),
							request.getAddress(), request.getPort());
					System.out.println("Duplicate request " + requestID);
					
				}

			}
		}

		// Balance enquiry
		/*if (msg.getRequestType() == RequestTypeEnum.BALANCE_ENQUIRY) {
			BalanceEnquiryMsgContent balEnquiryMsg = (BalanceEnquiryMsgContent) msg
					.getMsgContent();
			password = balEnquiryMsg.getPassword();
			accId = balEnquiryMsg.getAccountNumber();
			float res = this.enquiryAccount(balEnquiryMsg, password);
			BalanceEnquiryReplyMsgContent balEnquiryReplyMsg = null;
			if (res == -1) {
				balEnquiryReplyMsg = new BalanceEnquiryReplyMsgContent(
						ServiceResponseEnum.NON_EXISTING_ACCOUNT, accId);
			} else if (res == -2) {
				balEnquiryReplyMsg = new BalanceEnquiryReplyMsgContent(
						ServiceResponseEnum.WRONG_PASSWORD, accId);

			} else {
				balEnquiryReplyMsg = new BalanceEnquiryReplyMsgContent(
						ServiceResponseEnum.SUCCESS_BALANCE_ENQUIRY, accId);
			}
			reply = new Message(MessageTypeEnum.REPLY,
					RequestHandler.replyId++, balEnquiryReplyMsg);
			result = new DatagramPacket(reply.serialize(), reply.getLength(),
					request.getAddress(), request.getPort());

		}*/
		return result;
	}

	private long createAccount(CreateAccountMsgContent accountInfo) {
		long result = -1;
		try {
			result = this.accountService.Create(accountInfo);
		} catch (ServiceErrorException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} // return the id of new account
		return result;
	}

	private float enquiryAccount(BalanceEnquiryMsgContent account,
			String password) {
		float result = -1;

		try {
			result = this.accountService.EnquireBal(account, password);
		} catch (ServiceErrorException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return result;
	}

	private int closeAccount(String holderName, CloseAccountMsgContent account,
			String pass) {
		int result = -1;
		try {
			result = this.accountService.Close(holderName, account, pass);
		} catch (ServiceErrorException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return result;
	}

	private int withdrawAccount(WithdrawMsgContent account,
			float amountWithdraw, String password) {
		int result = -1;
		try {
			result = this.accountService.Withdraw(account, amountWithdraw,
					password);
		} catch (ServiceErrorException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return result;
	}

	private int depositAccount(DepositMsgContent account, float amountDeposit,
			String password) {
		int result = -1;
		try {
			result = this.accountService.Deposit(account, amountDeposit,
					password);
		} catch (ServiceErrorException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return result;
	}

	public ServiceResponseEnum transferService(TransferFundMsgContent account) {
		ServiceResponseEnum result = null;
		try {
			result = this.accountService.Transfer(account);
		} catch (ServiceErrorException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return result;
	}

	// check for duplicate requests
	public boolean checkDuplicateUID(DatagramPacket request) {
		byte[] data = request.getData();
		Message msg = new Message(data);
		int i;
		String requestAddress = request.getAddress().toString()
				.replaceFirst("/", "");
		long requestID = msg.getRequestId();
		RequestTypeEnum requestType = msg.getRequestType();
		
		//System.out.println("Requestype from the request is " + requestType);
		boolean duplicateUID = false;
		System.out.println(requestAddress + " " + requestID);
		if (UIDListLength == 0) {
			//System.out.println("Going to create UID with requesttype " + requestType + " because length of UIDListLength is " + UIDListLength);
			UIDList.add(new UID(requestAddress, requestID, requestType));
			UIDListLength++;
			// duplicateUID = false;
		} else {
			for (i = 0; i < UIDListLength; i++) {
				if (UIDList.get(i).clientAddress.equals(requestAddress)
						&& UIDList.get(i).requestID == requestID
						&& UIDList.get(i).requestType.equals(requestType)) {
					System.out.println("duplicated" + requestID);
					duplicateUID = true;
					break;
				}
			}
			if (!duplicateUID) {
				//System.out.println("Going to create UID with requesttype " + requestType);
				UIDList.add(new UID(requestAddress, requestID, requestType));
				UIDListLength++;
			}
		}
		return duplicateUID;
	}

	public static long increaseReplyID() {
		RequestHandler.replyId++;
		return RequestHandler.replyId;
	}

	// Monitor server
	public void MonitorServer(InetAddress ip, int port, long time) {
		// This is used to add the client into CallbackList and start timer
		System.out.println("Start timer at: " + new Date());
		MonitorTimerTask task = new MonitorTimerTask(ip, port);
		timer.schedule(task, time);
		monitor = new Callback(ip, port, time);
		AccountServiceImpl.getMonitorService().AddNewClient(monitor);

	}

	public void cancelTimer() {
		this.timer.cancel();
		System.out.println("Cancel timer");
	}

	// Task to handle the timer when it reach the delay (monitor interval)
	class MonitorTimerTask extends TimerTask {
		InetAddress ip;
		int port;

		public MonitorTimerTask(InetAddress ip, int port) {
			this.ip = ip;
			this.port = port;
		}

		// This task is used to delete the client out of CallbackList after
		// timer is finised
		@Override
		public void run() {
			AccountServiceImpl.getMonitorService().DeleteClient(monitor);
			System.out.println("Delete client at " + new Date());
			MonitorReplyMsgContent moniterReplyMsg = new MonitorReplyMsgContent(
					MonitorReplyTypeEnum.CLOSE);
			Message reply = new Message(MessageTypeEnum.REPLY,
					RequestHandler.replyId++, moniterReplyMsg);
			DatagramPacket result = new DatagramPacket(reply.serialize(),
					reply.getLength(), ip, port);
			try {
				SocketManager.getCurrent().SendPacket(result);
			} catch (SocketException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

	}
}
